<!DOCTYPE html>
<html>
<head>
<title>Sequence Diagram</title>
<meta name="description" content="A sequence diagram editor." />
<!-- Copyright 1998-2016 by Northwoods Software Corporation. -->
<meta charset="UTF-8">
<script src="go.js"></script>
<link href="../assets/css/goSamples.css" rel="stylesheet" type="text/css" />  <!-- you don't need to use this -->
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro' rel='stylesheet' type='text/css'>
<script src="goSamples.js"></script>  <!-- this is only for the GoJS Samples framework -->
<script id="code">
  function init() {
    if (window.goSamples) goSamples();  // init for these samples -- you don't need to call this
    var $ = go.GraphObject.make;

    myDiagram =
      $(go.Diagram, "myDiagramDiv", // must be the ID or reference to an HTML DIV
        {
          initialContentAlignment: go.Spot.Center,
          allowCopy: false,
          linkingTool: $(MessagingTool),  // defined below
          "resizingTool.isGridSnapEnabled": true,
          "draggingTool.gridSnapCellSize": new go.Size(1, MessageSpacing/4),
          "draggingTool.isGridSnapEnabled": true,
          // automatically extend Lifelines as Activities are moved or resized
          "SelectionMoved": ensureLifelineHeights,
          "PartResized": ensureLifelineHeights,
          "undoManager.isEnabled": true
        });

    // when the document is modified, add a "*" to the title and enable the "Save" button
    myDiagram.addDiagramListener("Modified", function(e) {
      var button = document.getElementById("SaveButton");
      if (button) button.disabled = !myDiagram.isModified;
      var idx = document.title.indexOf("*");
      if (myDiagram.isModified) {
        if (idx < 0) document.title += "*";
      } else {
        if (idx >= 0) document.title = document.title.substr(0, idx);
      }
    });

    // define the Lifeline Node template.
    myDiagram.groupTemplate =
      $(go.Group, "Vertical",
        {
          locationSpot: go.Spot.Bottom,
          locationObjectName: "HEADER",
          minLocation: new go.Point(0, 0),
          maxLocation: new go.Point(9999, 0),
          selectionObjectName: "HEADER"
        },
        new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
        $(go.Panel, "Auto",
          { name: "HEADER" },
          $(go.Shape, "Rectangle",
            { fill: $(go.Brush, "Linear", { 0: "#70A6FF", 1: "#A8C8FF" }),
              stroke: null }),
          $(go.TextBlock,
            { margin: 5,
              font: "400 10pt Source Sans Pro, sans-serif"},
            new go.Binding("text", "text"))
        ),
        $(go.Shape,
          {
            figure: "LineV",
            fill: null,
            stroke: "gray",
            strokeDashArray: [3, 3],
            width: 1,
            alignment: go.Spot.Center,
            portId: "",
            fromLinkable: true,
            fromLinkableDuplicates: true,
            toLinkable: true,
            toLinkableDuplicates: true,
            cursor: "pointer"
          },
          new go.Binding("height", "duration", computeLifelineHeight))
      );

    // define the Activity Node template
    myDiagram.nodeTemplate =
      $(go.Node,
        {
          locationSpot: go.Spot.Top,
          locationObjectName: "SHAPE",
          minLocation: new go.Point(NaN, LinePrefix-ActivityStart),
          maxLocation: new go.Point(NaN, 19999),
          selectionObjectName: "SHAPE",
          resizable: true,
          resizeObjectName: "SHAPE",
          resizeAdornmentTemplate:
            $(go.Adornment, "Spot",
              $(go.Placeholder),
              $(go.Shape,  // only a bottom resize handle
                { alignment: go.Spot.Bottom, cursor: "col-resize",
                desiredSize: new go.Size(6, 6), fill: "yellow" })
            )
        },
        new go.Binding("location", "", computeActivityLocation).makeTwoWay(backComputeActivityLocation),
        $(go.Shape, "Rectangle",
          {
            name: "SHAPE",
            fill: "white", stroke: "black",
            width: ActivityWidth,
            // allow Activities to be resized down to 1/4 of a time unit
            minSize: new go.Size(ActivityWidth, computeActivityHeight(0.25))
          },
          new go.Binding("height", "duration", computeActivityHeight).makeTwoWay(backComputeActivityHeight))
      );

    // define the Message Link template.
    myDiagram.linkTemplate =
      $(MessageLink,  // defined below
        { selectionAdorned: true, curviness: 0 },
        $(go.Shape, "Rectangle",
          { stroke: "black" }),
        $(go.Shape,
          { toArrow: "OpenTriangle", stroke: "black" }),
        $(go.TextBlock,
          {
            font: "400 9pt Source Sans Pro, sans-serif",
            segmentIndex: 0,
            segmentOffset: new go.Point(NaN, NaN),
            isMultiline: false,
            editable: true
          },
          new go.Binding("text", "text").makeTwoWay())
      );

    // create the graph by reading the JSON data saved in "mySavedModel" textarea element
    load();
  }

  function ensureLifelineHeights(e) {
    // iterate over all Activities (ignore Groups)
    var arr = myDiagram.model.nodeDataArray;
    var max = -1;
    for (var i = 0; i < arr.length; i++) {
      var act = arr[i];
      if (act.isGroup) continue;
      max = Math.max(max, act.start + act.duration);
    }
    if (max > 0) {
      // now iterate over only Groups
      for (var i = 0; i < arr.length; i++) {
        var gr = arr[i];
        if (!gr.isGroup) continue;
        if (max > gr.duration) {  // this only extends, never shrinks
          myDiagram.model.setDataProperty(gr, "duration", max);
        }
      }
    }
  }

  // some parameters
  var LinePrefix = 20;  // vertical starting point in document for all Messages and Activations
  var LineSuffix = 30;  // vertical length beyond the last message time
  var MessageSpacing = 20;  // vertical distance between Messages at different steps
  var ActivityWidth = 10;  // width of each vertical activity bar
  var ActivityStart = 5;  // height before start message time
  var ActivityEnd = 5;  // height beyond end message time

  function computeLifelineHeight(duration) {
    return LinePrefix + duration * MessageSpacing + LineSuffix;
  }

  function computeActivityLocation(act) {
    var groupdata = myDiagram.model.findNodeDataForKey(act.group);
    if (groupdata === null) return new go.Point();
    // get location of Lifeline's starting point
    var grouploc = go.Point.parse(groupdata.loc);
    return new go.Point(grouploc.x, convertTimeToY(act.start) - ActivityStart);
  }
  function backComputeActivityLocation(loc, act) {
    myDiagram.model.setDataProperty(act, "start", convertYToTime(loc.y + ActivityStart));
  }

  function computeActivityHeight(duration) {
    return ActivityStart + duration * MessageSpacing + ActivityEnd;
  }
  function backComputeActivityHeight(height) {
    return (height - ActivityStart - ActivityEnd) / MessageSpacing;
  }

  // time is just an abstract small non-negative integer
  // here we map between an abstract time and a vertical position
  function convertTimeToY(t) {
    return t * MessageSpacing + LinePrefix;
  }
  function convertYToTime(y) {
    return (y - LinePrefix) / MessageSpacing;
  }


  // a custom routed Link
  function MessageLink() {
    go.Link.call(this);
    this.time = 0;  // use this "time" value when this is the temporaryLink
  }
  go.Diagram.inherit(MessageLink, go.Link);

  /** @override */
  MessageLink.prototype.getLinkPoint = function(node, port, spot, from, ortho, othernode, otherport) {
    var p = port.getDocumentPoint(go.Spot.Center);
    var r = new go.Rect(port.getDocumentPoint(go.Spot.TopLeft),
                        port.getDocumentPoint(go.Spot.BottomRight));
    var op = otherport.getDocumentPoint(go.Spot.Center);

    var data = this.data;
    var time = data !== null ? data.time : this.time;  // if not bound, assume this has its own "time" property

    var aw = this.findActivityWidth(node, time);
    var x = (op.x > p.x ? p.x + aw / 2 : p.x - aw / 2);
    var y = convertTimeToY(time);
    return new go.Point(x, y);
  };

  MessageLink.prototype.findActivityWidth = function(node, time) {
    var aw = ActivityWidth;
    if (node instanceof go.Group) {
      // see if there is an Activity Node at this point -- if not, connect the link directly with the Group's lifeline
      if (!node.memberParts.any(function(mem) {
              var act = mem.data;
              return (act !== null && act.start <= time && time <= act.start + act.duration);
      })) {
        aw = 0;
      }
    }
    return aw;
  };

  /** @override */
  MessageLink.prototype.getLinkDirection = function(node, port, linkpoint, spot, from, ortho, othernode, otherport) {
    var p = port.getDocumentPoint(go.Spot.Center);
    var op = otherport.getDocumentPoint(go.Spot.Center);
    var right = op.x > p.x;
    return right ? 0 : 180;
  };

  /** @override */
  MessageLink.prototype.computePoints = function() {
    if (this.fromNode === this.toNode) {  // also handle a reflexive link as a simple orthogonal loop
      var data = this.data;
      var time = data !== null ? data.time : this.time;  // if not bound, assume this has its own "time" property
      var p = this.fromNode.port.getDocumentPoint(go.Spot.Center);
      var aw = this.findActivityWidth(this.fromNode, time);

      var x = p.x + aw / 2;
      var y = convertTimeToY(time);
      this.clearPoints();
      this.addPoint(new go.Point(x, y));
      this.addPoint(new go.Point(x + 50, y));
      this.addPoint(new go.Point(x + 50, y + 5));
      this.addPoint(new go.Point(x, y + 5));
      return true;
    } else {
      return go.Link.prototype.computePoints.call(this);
    }
  }

  // end MessageLink


  // a custom LinkingTool that fixes the "time" (i.e. the Y coordinate)
  // for both the temporaryLink and the actual newly created Link
  function MessagingTool() {
    go.LinkingTool.call(this);
    var $ = go.GraphObject.make;
    this.temporaryLink =
      $(MessageLink,
        $(go.Shape, "Rectangle",
          { stroke: "magenta", strokeWidth: 2 }),
        $(go.Shape,
          { toArrow: "OpenTriangle", stroke: "magenta" }));
  };
  go.Diagram.inherit(MessagingTool, go.LinkingTool);

  /** @override */
  MessagingTool.prototype.doActivate = function() {
    go.LinkingTool.prototype.doActivate.call(this);
    var time = convertYToTime(this.diagram.firstInput.documentPoint.y);
    this.temporaryLink.time = Math.ceil(time);  // round up to an integer value
  };

  /** @override */
  MessagingTool.prototype.insertLink = function(fromnode, fromport, tonode, toport) {
    var newlink = go.LinkingTool.prototype.insertLink.call(this, fromnode, fromport, tonode, toport);
    if (newlink !== null) {
      var model = this.diagram.model;
      // specify the time of the message
      var start = this.temporaryLink.time;
      var duration = 1;
      newlink.data.time = start;
      model.setDataProperty(newlink.data, "text", "msg");
      // and create a new Activity node data in the "to" group data
      var newact = {
        group: newlink.data.to,
        start: start,
        duration: duration
      };
      model.addNodeData(newact);
      // now make sure all Lifelines are long enough
      ensureLifelineHeights();
    }
    return newlink;
  };
  // end MessagingTool


  // Show the diagram's model in JSON format
  function save() {
    document.getElementById("mySavedModel").value = myDiagram.model.toJson();
    myDiagram.isModified = false;
  }
  function load() {
    myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
  }
</script>
</head>
<body onload="init()" >
<div id="sample">
  <div id="myDiagramDiv" style="border: solid 1px black; width: 100%; height: 400px"></div>
  <p>
    A <em>sequence diagram</em> is an interaction diagram that shows how entities operate with one another and in what order.
    In this sample, we show the interaction between different people in a restaurant.
  </p>
  <p>
    The diagram uses the <a>Diagram.groupTemplate</a> for "lifelines,"
    <a>Diagram.nodeTemplate</a> for "activities," and <a>Diagram.linkTemplate</a> for "messages" between the entities.
    Also featured are a custom Link class and custom <a>LinkingTool</a> to draw links
    between lifelines and create activities at the end of the new link. Nodes use a binding function on the location
    property to ensure they are anchored to their lifeline.
  </p>
  <div>
    <div>
      <button id="SaveButton" onclick="save()">Save</button>
      <button onclick="load()">Load</button>
      Diagram Model saved in JSON format:
    </div>
    <textarea id="mySavedModel" style="width:100%;height:240px">
{ "class": "go.GraphLinksModel",
  "nodeDataArray": [
{"key":"Fred", "text":"Fred: Patron", "isGroup":true, "loc":"0 0", "duration":9},
{"key":"Bob", "text":"Bob: Waiter", "isGroup":true, "loc":"100 0", "duration":9},
{"key":"Hank", "text":"Hank: Cook", "isGroup":true, "loc":"200 0", "duration":9},
{"key":"Renee", "text":"Renee: Cashier", "isGroup":true, "loc":"300 0", "duration":9},
{"group":"Bob", "start":1, "duration":2},
{"group":"Hank", "start":2, "duration":3},
{"group":"Fred", "start":3, "duration":1},
{"group":"Bob", "start":5, "duration":1},
{"group":"Fred", "start":6, "duration":2},
{"group":"Renee", "start":8, "duration":1}
 ],
  "linkDataArray": [
{"from":"Fred", "to":"Bob", "text":"order", "time":1},
{"from":"Bob", "to":"Hank", "text":"order food", "time":2},
{"from":"Bob", "to":"Fred", "text":"serve drinks", "time":3},
{"from":"Hank", "to":"Bob", "text":"finish cooking", "time":5},
{"from":"Bob", "to":"Fred", "text":"serve food", "time":6},
{"from":"Fred", "to":"Renee", "text":"pay", "time":8}
 ]}
    </textarea>
  </div>
</div>
</body>
</html>
